文章摘要
加载中...|
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结 投诉

概述

大语言模型虽然能力强大,但存在两个核心问题:

  1. 知识截止:训练数据有时间限制,不知道最新信息
  2. 幻觉问题:可能编造不存在的事实

RAG(Retrieval-Augmented Generation,检索增强生成)通过让 LLM 检索外部知识库来回答问题,有效解决了这些问题。本文将深入解析 RAG 技术的原理和实践。

什么是 RAG

RAG 的定义

RAG 是一种结合了信息检索生成式 AI的技术,它的工作流程是:

text
┌─────────────────────────────────────────────────────────┐
│                    RAG 工作流程                           │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  用户问题                                                │
│     │                                                    │
│     ▼                                                    │
│  ┌─────────────────────────────────────┐               │
│  │      检索相关文档 (Retriever)        │               │
│  └─────────────────────────────────────┘               │
│     │                                                    │
│     ▼                                                    │
│  ┌─────────────────────────────────────┐               │
│  │  将检索结果作为上下文 (Context)      │               │
│  └─────────────────────────────────────┘               │
│     │                                                    │
│     ▼                                                    │
│  ┌─────────────────────────────────────┐               │
│  │    LLM 基于上下文生成回答 (Generator) │               │
│  └─────────────────────────────────────┘               │
│     │                                                    │
│     ▼                                                    │
│  最终回答 (带来源引用)                                    │
│                                                         │
└─────────────────────────────────────────────────────────┘

RAG 的优势

优势说明
实时信息可以获取最新知识
可解释性回答可以引用来源
私有数据可以利用企业私有数据
减少幻觉基于真实文档回答
成本低不需要微调模型

RAG vs Fine-tuning

text
┌─────────────────────────────────────────────────────────┐
│                RAG vs Fine-tuning                        │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  RAG 的优势:                                            │
│  • 实时更新知识                                          │
│  • 可以引用来源                                          │
│  • 实现简单成本低                                        │
│  • 适合知识密集型任务                                    │
│                                                         │
│  Fine-tuning 的优势:                                    │
│  • 学习特定风格/格式                                      │
│  • 减少延迟(不需要检索)                                 │
│  • 适合行为调整                                          │
│                                                         │
│  组合使用:                                              │
│  RAG + Fine-tuning = 最佳效果                            │
│                                                         │
└─────────────────────────────────────────────────────────┘

选择建议

场景推荐方案
需要最新信息RAG
需要引用来源RAG
私有数据问答RAG
特定输出格式Fine-tuning
特定说话风格Fine-tuning
复杂推理任务RAG + Fine-tuning

RAG 的核心流程

完整流程图

text
┌─────────────────────────────────────────────────────────┐
│                    RAG 完整流程                           │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  【准备阶段 - 离线】                                      │
│                                                         │
│  文档 → 加载 → 分块 → Embedding → 向量数据库              │
│                                                         │
│  【查询阶段 - 在线】                                      │
│                                                         │
│  用户问题 → Embedding → 向量检索 → Top-K 文档              │
│                                    │                     │
│                                    ▼                     │
│  ┌─────────────────────────────────────────────────┐    │
│  │  Prompt: "基于以下文档回答问题..."               │    │
│  │                                                 │    │
│  │  [文档1]                                         │    │
│  │  [文档2]                                         │    │
│  │  [文档3] + 问题                                  │    │
│  └─────────────────────────────────────────────────┘    │
│              │                                           │
│              ▼                                           │
│         LLM 生成回答                                     │
│                                                         │
└─────────────────────────────────────────────────────────┘

步骤详解

1. 文档加载 (Document Loading)

python
from langchain.document_loaders import (
    TextLoader,
    PyPDFLoader,
    DirectoryLoader,
    WebBaseLoader
)

# 加载单个文本文件
loader = TextLoader("doc.txt")
documents = loader.load()

# 加载 PDF
pdf_loader = PyPDFLoader("report.pdf")
pdf_docs = pdf_loader.load()

# 加载整个目录
dir_loader = DirectoryLoader(
    "./docs",
    glob="**/*.md",
    loader_cls=TextLoader
)
all_docs = dir_loader.load()

# 加载网页
web_loader = WebBaseLoader([
    "https://example.com/doc1",
    "https://example.com/doc2"
])
web_docs = web_loader.load()

2. 文档分块 (Chunking)

python
from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    TokenTextSplitter
)

# 字符分块
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,      # 每块大小
    chunk_overlap=200,    # 重叠大小
    length_function=len,
    separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]
)

chunks = text_splitter.split_documents(documents)

# Token 分块
token_splitter = TokenTextSplitter(
    chunk_size=500,       # token 数量
    chunk_overlap=50,
    encoding_name="cl100k_base"
)

token_chunks = token_splitter.split_documents(documents)

3. 向量化 (Embedding)

python
from langchain_openai import OpenAIEmbeddings
from langchain_community.embeddings import HuggingFaceEmbeddings

# OpenAI Embeddings
openai_embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small"
)

# 本地 Embeddings
local_embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/all-MiniLM-L6-v2"
)

# 生成向量
texts = [chunk.page_content for chunk in chunks]
vectors = openai_embeddings.embed_documents(texts)

4. 向量存储 (Vector Store)

python
from langchain.vectorstores import Chroma, Pinecone, FAISS
from langchain_community.vectorstores import Qdrant

# Chroma (本地,最简单)
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=openai_embeddings,
    persist_directory="./chroma_db"
)

# FAISS (本地,高效)
faiss_store = FAISS.from_documents(
    documents=chunks,
    embedding=openai_embeddings
)
faiss_store.save_local("faiss_index")

# Pinecone (云端)
pinecone_store = Pinecone.from_documents(
    documents=chunks,
    embedding=openai_embeddings,
    index_name="my-rag-index"
)

# Qdrant (本地/云端)
qdrant_store = Qdrant.from_documents(
    documents=chunks,
    embedding=openai_embeddings,
    url="http://localhost:6333",
    collection_name="my_collection"
)

5. 检索 (Retrieval)

python
# 相似度检索
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4}  # 返回 Top-4
)

# 相似度得分检索
retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={
        "k": 4,
        "score_threshold": 0.7  # 相似度阈值
    }
)

# MMR 检索(多样性)
retriever = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 4, "lambda_mult": 0.5}
)

# 执行检索
results = retriever.get_relevant_documents("什么是 RAG?")

for i, doc in enumerate(results):
    print(f"文档 {i+1}:")
    print(f"内容: {doc.page_content[:100]}...")
    print(f"元数据: {doc.metadata}")
    print()

6. 生成 (Generation)

python
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate

# 自定义 Prompt
prompt_template = """请基于以下文档回答问题。如果文档中没有相关信息,请说"我不知道"。

文档内容:
{context}

问题: {question}

回答:"""

PROMPT = PromptTemplate(
    template=prompt_template,
    input_variables=["context", "question"]
)

# 创建 RAG 链
llm = ChatOpenAI(model="gpt-4o", temperature=0)

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True,
    chain_type_kwargs={"prompt": PROMPT}
)

# 查询
result = qa_chain.invoke({"query": "什么是 RAG?"})

print("回答:", result['result'])
print("\n来源文档:")
for doc in result['source_documents']:
    print(f"- {doc.metadata.get('source', 'unknown')}")

文档分块策略

分块策略对比

策略优点缺点适用场景
固定大小简单高效可能切断语义通用文档
语义分块保持语义完整计算成本高复杂文档
递归分块平衡效果和性能需要调参大多数场景
父文档检索保留上下文存储开销大长文档

递归字符分块(推荐)

python
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,        # 目标块大小
    chunk_overlap=200,      # 重叠大小(保持上下文)
    length_function=len,
    separators=[
        "\n\n",    # 段落
        "\n",      # 行
        "。",      # 中文句号
        "!",      # 中文感叹号
        "?",      # 中文问号
        ",",      # 中文逗号
        " ",       # 空格
        ""         # 字符(最后手段)
    ]
)

chunks = text_splitter.split_documents(documents)

父文档检索

python
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore

# 子文档(用于检索)
child_splitter = RecursiveCharacterTextSplitter(
    chunk_size=400
)

# 父文档(用于生成)
parent_splitter = RecursiveCharacterTextSplitter(
    chunk_size=2000
)

# 创建检索器
retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=InMemoryStore(),
    child_splitter=child_splitter,
    parent_splitter=parent_splitter
)

# 添加文档
retriever.add_documents(documents)

# 检索时返回父文档
results = retriever.get_relevant_documents("问题")

分块大小建议

文档类型Chunk SizeOverlap
短文本256-51250
一般文章512-1024100-200
长文档1024-2048200-400
代码500-100050-100

检索策略

1. 相似度检索

python
# 基础相似度检索
retriever = vectorstore.as_retriever(
    search_kwargs={"k": 5}
)

# 带阈值
retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={
        "k": 5,
        "score_threshold": 0.7
    }
)

2. MMR 检索(多样性)

text
┌─────────────────────────────────────────────────────────┐
│                    MMR 原理                              │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  目标:平衡相关性和多样性                                │
│                                                         │
│  lambda → 1: 只看相关性(可能重复)                       │
│  lambda → 0: 只看多样性(可能不相关)                     │
│  lambda = 0.5: 平衡(推荐)                              │
│                                                         │
└─────────────────────────────────────────────────────────┘
python
retriever = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 5,
        "lambda_mult": 0.5
    }
)

3. 混合检索

python
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

# 向量检索
vector_retriever = vectorstore.as_retriever(
    search_kwargs={"k": 5}
)

# 关键词检索 (BM25)
bm25_retriever = BM25Retriever.from_documents(
    documents=chunks
)
bm25_retriever.k = 5

# 混合检索
ensemble_retriever = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.5, 0.5]  # 权重
)

4. 重排序 (Reranking)

python
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

# 加载重排序模型
reranker = HuggingFaceCrossEncoder(
    model_name="BAAI/bge-reranker-base"
)

# 检索更多文档
docs = retriever.get_relevant_documents("问题", k=20)

# 重排序
reranked_docs = reranker.compress_documents(
    documents=docs,
    query="问题"
)

# 取 Top-K
final_docs = reranked_docs[:5]

RAG 优化技巧

1. 查询扩展

python
def query_expansion(query: str, llm) -> list:
    """扩展查询为多个变体"""
    prompt = f"""生成 3 个不同方式表达的查询,意图与以下查询相同:
查询: {query}

只输出查询,每行一个。"""

    result = llm.invoke(prompt)
    queries = [query] + result.content.split('\n')
    return [q.strip() for q in queries if q.strip()]

# 使用扩展查询
queries = query_expansion("如何提高 RAG 效果?", llm)

all_docs = []
for q in queries:
    docs = retriever.get_relevant_documents(q, k=2)
    all_docs.extend(docs)

# 去重
unique_docs = list({d.page_content: d for d in all_docs}.values())

2. 元数据过滤

python
# 自定义检索器
def metadata_filter_retrieval(query: str, filters: dict):
    """结合元数据过滤的检索"""
    # 先按相似度检索
    docs = vectorstore.similarity_search(query, k=20)

    # 再按元数据过滤
    filtered_docs = []
    for doc in docs:
        match = True
        for key, value in filters.items():
            if doc.metadata.get(key) != value:
                match = False
                break
        if match:
            filtered_docs.append(doc)

    return filtered_docs[:5]

# 使用
results = metadata_filter_retrieval(
    "API 使用方法",
    filters={"category": "技术文档", "year": 2024}
)

3. 上下文压缩

python
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

compressor = LLMChainExtractor.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=retriever
)

# 只返回与问题相关的内容
compressed_docs = compression_retriever.get_relevant_documents("问题")

常见问题与解决方案

问题 1:检索不相关

原因:

  • 分块不合理
  • Embedding 模型不匹配
  • 查询表达不清

解决方案:

python
# 1. 优化分块
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,        # 更小的块
    chunk_overlap=100,
    separators=["\n\n", "\n", "。", "!", "?"]
)

# 2. 使用领域 Embeddings
from langchain_community.embeddings import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-large-zh-v1.5"  # 中文优化
)

# 3. 查询重写
expanded_queries = query_expansion(original_query, llm)

问题 2:回答不完整

原因:

  • 检索的文档不够
  • 上下文窗口限制

解决方案:

python
# 1. 增加检索数量
retriever = vectorstore.as_retriever(
    search_kwargs={"k": 10}  # 增加 k 值
)

# 2. 使用父文档检索
parent_retriever = ParentDocumentRetriever(...)

# 3. 迭代检索
def iterative_retrieval(query, max_iterations=3):
    context = ""
    for i in range(max_iterations):
        docs = retriever.get_relevant_documents(query)
        new_info = "\n".join([d.page_content for d in docs])
        if new_info not in context:
            context += "\n" + new_info

        # 检查是否足够
        if len(context) > 3000:
            break

        # 生成子问题
        query = llm.invoke(f"基于: {context}\n生成下一个子问题")
    return context

问题 3:回答仍有幻觉

原因:

  • 检索文档质量差
  • Prompt 没有约束

解决方案:

python
# 1. 改进 Prompt
strict_prompt = """请严格基于以下文档回答问题。
如果文档中没有答案,请说"根据提供的文档,我无法回答这个问题"。

文档:
{context}

问题: {question}

回答:"""

# 2. 添加来源验证
def add_citations(response, source_docs):
    """在回答中添加引用"""
    citations = "\n\n来源:\n"
    for doc in source_docs:
        citations += f"- {doc.metadata.get('source', 'unknown')}\n"
    return response + citations

# 3. 使用 Reranker 过滤低质量文档
reranker = HuggingFaceCrossEncoder(...)

RAG 系统评估

评估指标

指标说明计算方法
Precision检索准确率相关文档数 / 检索文档数
Recall检索召回率检索到相关文档数 / 总相关文档数
F1综合指标2 × P × R / (P + R)
MRR平均倒数排名1 / 第一个相关文档排名
NDCG排序质量考虑位置的评分

评估代码示例

python
from ragas import evaluate
from datasets import Dataset

# 准备评估数据
test_data = {
    "question": ["什么是 RAG?", "如何优化 RAG?"],
    "answer": ["RAG 是...", "优化方法包括..."],
    "contexts": [
        ["文档1内容", "文档2内容"],
        ["文档3内容", "文档4内容"]
    ],
    "ground_truth": ["RAG 的正确答案", "优化方法的正确答案"]
}

dataset = Dataset.from_dict(test_data)

# 评估
result = evaluate(
    dataset=dataset,
    metrics=[
        "faithfulness",     # 忠实度
        "answer_relevancy", # 相关性
        "context_precision",# 上下文精确度
        "context_recall"    # 上下文召回率
    ]
)

print(result)

小结

RAG 是让 LLM 能够利用外部知识库的关键技术:

核心要点

  1. RAG 流程

    • 文档加载 → 分块 → 向量化 → 存储
    • 查询向量化 → 检索 → 生成回答
  2. 关键技巧

    • 合理的分块策略(递归分块推荐)
    • 混合检索(向量 + 关键词)
    • 重排序提高精度
    • 查询扩展和重写
  3. 优化方向

    • 检索质量:更好的分块、Embedding、重排序
    • 生成质量:优化的 Prompt、上下文压缩
    • 系统性能:缓存、批量处理、更快的模型
  4. 与微调的关系

    • RAG 和 Fine-tuning 是互补的
    • 可以组合使用获得最佳效果

下一篇文章将详细介绍向量数据库,它是 RAG 系统的核心组件。

赞赏博主
评论 隐私政策